/*
 * RetractPanel.java
 *
 * Created on June 23, 2007, 5:47 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package com.bff.slimserver.musicjukebox.ui.components;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.RootPaneContainer;
import javax.swing.SwingConstants;
import javax.swing.Timer;

/**
 *
 * @author Bill
 */
public class RetractPanel extends JPanel {
    /**
     * Constant to indicate a top-down display.
     */
    public static final int TOP_DOWN = SwingConstants.TOP;
    /**
     * Constant to indicate a bottom-up display.
     */
    public static final int BOTTOM_UP = SwingConstants.BOTTOM;
    /**
     * Constant to indicate a left-to-right display.
     */
    public static final int LEFT_TO_RIGHT = SwingConstants.LEFT;
    /**
     * Constant to indicate a right-to-left display.
     */
    public static final int RIGHT_TO_LEFT = SwingConstants.RIGHT;
    /**
     * The parent container.
     */
    private RootPaneContainer parent;
    /**
     * The display direction.
     */
    private int direction = TOP_DOWN;
    /**
     * The content component.
     */
    private JComponent contents;
    /**
     * The x position.
     */
    private int x = 0;
    /**
     * The y position.
     */
    private int y = 0;
    /**
     * The percentage of the component that is visible.
     */
    private float percentVisible = 0.0f;
    /**
     * The animation timer.
     */
    private Timer timer;
    /**
     * The animation delay in milliseconds.
     */
    private int delay = 40;
    /**
     * The component to use as a tab.
     */
    private JComponent tabComponent = null;
    /**
     * The tab component placement.
     * @see  SwingConstants#TOP
     * @see  SwingConstants#BOTTOM
     * @see  SwingConstants#LEFT
     * @see  SwingConstants#RIGHT
     */
    private int tabPlacement = SwingConstants.BOTTOM;
    /**
     * The tab component alignment.
     * @see  Component#TOP_ALIGNMENT
     * @see  Component#BOTTOM_ALIGNMENT
     * @see  Component#CENTER_ALIGNMENT
     * @see  Component#LEFT_ALIGNMENT
     * @see  Component#RIGHT_ALIGNMENT
     */
    private float tabAlignment = Component.CENTER_ALIGNMENT;
    
    /**
     * Creates a new <code>RetractPanel</code> object.
     * @param  contents   the content component
     * @param  direction  the display direction
     */
    public RetractPanel(JComponent contents, int direction) {
        this.direction = direction;
        setLayout(new BorderLayout());
        setOpaque(true);
        setBorder(BorderFactory.createLineBorder(Color.black));
        
        JPanel p = new JPanel() {
            public void doLayout() {
                Component c = getComponent(0);
                Dimension cs = c.getPreferredSize();
                int d = RetractPanel.this.getDirection();
                float v = RetractPanel.this.percentVisible;
                int _x = 0;
                int _y = 0;
                
                if(d == TOP_DOWN) {
                    _y = (int)(cs.height * v) - cs.height;
                } else if(d == LEFT_TO_RIGHT) {
                    _x = (int)(cs.width * v) - cs.width;
                }
                
                c.setBounds(_x, _y, cs.width, cs.height);
            }
        };
        
        p.setLayout(null);
        p.setOpaque(false);
        this.contents = contents;
        this.contents.setOpaque(true);
        this.contents.setBackground(Color.green);
        p.add(this.contents);
        add(p, BorderLayout.CENTER);
    }
    
    /**
     * Installs the component in the palette layer of the specified
     * root pane container.
     * @param  parent  the root pane container
     * @param  x       the x-position of the component
     * @param  y       the y-position of the component
     */
    public void install(RootPaneContainer parent, int x, int y) {
        this.parent = parent;
        setPosition(x, y);
        setBounds(x, y, 0, 0);
        this.parent.getLayeredPane().add(this, JLayeredPane.PALETTE_LAYER);
        
        if(this.tabComponent != null) {
            this.tabComponent.setBounds(0, 0, 0, 0);
            this.parent.getLayeredPane().add(this.tabComponent);
        }
        
        setMinimized(false);
    }
    
    /**
     * Gets the component to use as a tab.
     * @return  the component used as a tab
     */
    public JComponent getTabComponent() {
        return this.tabComponent;
    }
    
    /**
     * Sets the component to use as a tab.
     * @param  c  the component used as a tab
     */
    public void setTabComponent(JComponent c) {
        if(this.tabComponent != null && this.parent != null) {
            this.parent.getLayeredPane().remove(this.tabComponent);
        }
        
        this.tabComponent = c;
        
        if(this.tabComponent != null && this.parent != null) {
            this.tabComponent.setBounds(0, 0, 0, 0);
            this.parent.getLayeredPane().add(this.tabComponent);
        }
        
        show(this.percentVisible);
    }
    
    /**
     * Gets the tab component placement.
     * @return  the component placement
     * @see  SwingConstants#TOP
     * @see  SwingConstants#BOTTOM
     * @see  SwingConstants#LEFT
     * @see  SwingConstants#RIGHT
     */
    public int getTabPlacement() {
        return this.tabPlacement;
    }
    
    /**
     * Sets the tab component placement.
     * @param  p  the component placement
     * @see  SwingConstants#TOP
     * @see  SwingConstants#BOTTOM
     * @see  SwingConstants#LEFT
     * @see  SwingConstants#RIGHT
     */
    public void setTabPlacement(int p) {
        if(p != SwingConstants.TOP && p != SwingConstants.BOTTOM &&
                p != SwingConstants.LEFT && p != SwingConstants.RIGHT) {
            throw new IllegalArgumentException("Invalid tab placement.");
        }
        
        this.tabPlacement = p;
        show(this.percentVisible);
    }
    
    /**
     * Gets the tab component alignment.
     * @return  the component alignment
     * @see  Component#TOP_ALIGNMENT
     * @see  Component#BOTTOM_ALIGNMENT
     * @see  Component#CENTER_ALIGNMENT
     * @see  Component#LEFT_ALIGNMENT
     * @see  Component#RIGHT_ALIGNMENT
     */
    public float getTabAlignment() {
        return this.tabAlignment;
    }
    
    /**
     * Sets the tab component alignment.  The alignment is a float value
     * representing a percentage from top to bottom or left to right of the
     * main panel to place the tab component.
     * @param  a  the component alignment
     * @see  Component#TOP_ALIGNMENT
     * @see  Component#BOTTOM_ALIGNMENT
     * @see  Component#CENTER_ALIGNMENT
     * @see  Component#LEFT_ALIGNMENT
     * @see  Component#RIGHT_ALIGNMENT
     */
    public void setTabAlignment(float a) {
        if(a < 0.0f || a > 1.0f) {
            throw new IllegalArgumentException("Invalid tab alignment.");
        }
        
        this.tabAlignment = a;
        show(this.percentVisible);
    }
    
    /**
     * Sets the visibility of the component.
     * @param  v  if true, sets the component to be visible
     */
    public void setVisible(boolean v) {
        if(this.tabComponent != null) {
            this.tabComponent.setVisible(v);
        }
        
        super.setVisible(v);
        
        if(v) {
            show(this.percentVisible);
        }
    }
    
    /**
     * Gets the direction the component expands.
     * @return  the direction the component expands
     * @see  #TOP_DOWN
     * @see  #BOTTOM_UP
     * @see  #LEFT_TO_RIGHT
     * @see  #RIGHT_TO_LEFT
     */
    public int getDirection() {
        return this.direction;
    }
    
    /**
     * Sets the direction the component expands.
     * @param  d  the direction the component expands
     * @see  #TOP_DOWN
     * @see  #BOTTOM_UP
     * @see  #LEFT_TO_RIGHT
     * @see  #RIGHT_TO_LEFT
     */
    public void setDirection(int d) {
        if(d != TOP_DOWN && d != BOTTOM_UP && d != LEFT_TO_RIGHT &&
                d != RIGHT_TO_LEFT) {
            throw new IllegalArgumentException("Invalid direction.");
        }
        
        this.direction = d;
        show(this.percentVisible);
    }
    
    /**
     * Gets the position of the component.
     * @return  the position of the component
     */
    public Point getPosition() {
        return new Point(this.x, this.y);
    }
    
    /**
     * Sets the position of the component.  Use this instead of setLocation().
     * @param  p  the position of the component
     */
    public void setPosition(Point p) {
        setPosition(p.x, p.y);
    }
    
    /**
     * Sets the position of the component.  Use this instead of setLocation().
     * @param  x  the x-position of the component
     * @param  y  the y-position of the component
     */
    public void setPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    /**
     * Toggles the state between minimized or maximized state.
     * @param  animated  if the panel should be animated
     */
    public void toggle(boolean animated) {
        if(isMinimized()) {
            setMaximized(animated);
        } else {
            setMinimized(animated);
        }
    }
    
    /**
     * Checks if the panel is at it's minimized state.
     * @return  true if the panel is at it's minimized state
     */
    public boolean isMinimized() {
        return (this.percentVisible == 0.0f);
    }
    
    /**
     * Sets the panel to it's minimized state.
     * @param  animated  if the minimizing should be animated
     */
    public void setMinimized(boolean animated) {
        if(animated) {
            animate(0.0f);
        } else {
            show(0.0f);
        }
    }
    
    /**
     * Checks if the panel is at it's maximized state.
     * @return  true if the panel is at it's maximized state
     */
    public boolean isMaximized() {
        return (this.percentVisible == 1.0f);
    }
    
    /**
     * Sets the panel to it's maximized state.
     * @param  animated  if the maximizing should be animated
     */
    public void setMaximized(boolean animated) {
        if(animated) {
            animate(1.0f);
        } else {
            show(1.0f);
        }
    }
    
    /**
     * Animates the expansion or contraction of the panel to the given size.
     * @param  to  the end-state visible percentage
     */
    private void animate(final float to) {
        killTimer(); // kill timer if running
        this.timer = new Timer(delay,
                new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                boolean stop = false;
                
                if(to == 1.0f) {
                    show(RetractPanel.this.percentVisible + 0.05f);
                    
                    if(RetractPanel.this.percentVisible >= 1.0f) {
                        stop = true;
                    }
                } else {
                    show(RetractPanel.this.percentVisible - 0.05f);
                    
                    if(RetractPanel.this.percentVisible <= 0.0f) {
                        stop = true;
                    }
                }
                
                if(stop) {
                    killTimer();
                    show(to);
                }
            }
        });
        this.timer.setRepeats(true);
        this.timer.start();
    }
    
    /**
     * Stops and voids the animation timer, if one is running.
     */
    private void killTimer() {
        if(this.timer != null) {
            if(this.timer.isRunning()) {
                this.timer.stop();
            }
            
            this.timer = null;
        }
    }
    
    /**
     * Sets the panel to display the given visible percentage.  The value is
     * a ratio from 0.0 to 1.0.  Generally, this is not used.  Instead, use:
     * <ul>
     *   <li><code>expand()</code> or <code>contract()</code> to show or
     *       hide the component in an animated way.</li>
     *   <li>use <code>setMaximized()</code> or <code>setMinimized()</code>
     *       to show or hide the component directly.</li>
     * </ul>
     * @param  pct  the percentage of the component visible
     */
    public void show(float pct) {
        if(pct < 0.0f) {
            pct = 0.0f;
        } else if(pct > 1.0f) {
            pct = 1.0f;
        }
        
        Dimension size = getPreferredSize();
        
        this.percentVisible = pct;
        
        int _x = this.x;
        int _y = this.y;
        int _w = size.width;
        int _h = size.height;
        
        if(this.direction == TOP_DOWN || this.direction == BOTTOM_UP) {
            _h = (int)(size.height * this.percentVisible);
            
            if(this.direction == BOTTOM_UP) {
                _y = (_y + size.height) - _h;
            }
        } else if(this.direction == LEFT_TO_RIGHT ||
                this.direction == RIGHT_TO_LEFT) {
            _w = (int)(size.width * this.percentVisible);
            
            if(this.direction == RIGHT_TO_LEFT) {
                _x = (_x + size.width) - _w;
            }
        }
        
        setBounds(_x, _y, _w, _h);
        revalidate();
        updateTab();
    }
    
    /**
     * Updates the tab component placement.
     */
    private void updateTab() {
        if(this.tabComponent == null) {
            return;
        }
        
        Rectangle bounds = getBounds();
        Dimension size = this.tabComponent.getPreferredSize();
        int _x = this.x;
        int _y = this.y;
        int _w = size.width;
        int _h = size.height;
        
        if(this.tabPlacement == SwingConstants.TOP ||
                this.tabPlacement == SwingConstants.BOTTOM) {
            _x = bounds.x +
                    (int)((bounds.width - size.width) * this.tabAlignment);
            
            if(this.tabPlacement == SwingConstants.TOP) {
                _y = bounds.y - size.height;
            } else {
                _y = bounds.y + bounds.height;
            }
        } else if(this.tabPlacement == SwingConstants.LEFT ||
                this.tabPlacement == SwingConstants.RIGHT) {
            if(this.tabPlacement == SwingConstants.LEFT) {
                _x = bounds.x - size.width;
            } else {
                _x = bounds.x + bounds.width;
            }
            
            _y = bounds.y +
                    (int)((bounds.height - size.height) * this.tabAlignment);
        }
        
        this.tabComponent.setBounds(_x, _y, _w, _h);
    }
    
    /**
     * Gets the preferred size of the component.
     * @return  the preferred size of the component
     */
    public Dimension getPreferredSize() {
        Dimension size = new Dimension(contents.getPreferredSize());
        Insets insets = getInsets();
        
        size.width += insets.left + insets.right;
        size.height += insets.top + insets.bottom;
        
        return size;
    }
}
